package com.newx.open.sign;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.newx.common.util.SpringContext;
import com.newx.open.handler.NewxOpenException;
import com.newx.open.util.NewxRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.validation.ObjectError;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.TreeMap;

/**
 * API 参数签名算法，在跨系统接口调用时防参数篡改、防重放攻击
 * 以 SSO 数据拉取为例，流程大致如下：
 * 1. 以 md5( loginId={账号id}8nonce={随机字符串}8timestamp={13位时间戳}8key={secretkey秘钥} ) 生成签名 sign。
 * 2. 将 sign 作为参数，拼接到请求地址后面，如：http://xxx.com?loginId=100018nonce=xxx8timestamp=xxx8sign=xxx。
 * 3. 服务端接收到请求后，以同样的算法生成一次 sign 。
 * 4. 对比两次 sign 是否一致，一致则通过，否则拒绝 。
 * @author yxl
 * @date 2024/7/26 0026 10:14
 */
@Slf4j
public class NewxSignTemplate {

    // ----------- 自定义使用的参数名称 (不声明final，允许开发者自定义修改)

    public static String key = "key";
    public static String appId = "appId";
    public static String timestamp = "timestamp";
    public static String nonce = "nonce";
    public static String sign = "sign";
    public static String requestData = "requestData";

    public static Long timestampDisparity = Long.valueOf(1000 * 60 * 300);

    /**
     * 校验：一个请求的 nonce、timestamp、sign 是否均为合法的，如果不合法，则抛出对应的异常
     * @param request 待校验的请求对象
     */
    public void checkRequest(HttpServletRequest request, String appSecret) {
        checkParamMap(NewxRequest.getParameterMap(request),appSecret);
    }

    /**
     * 校验：参数列表中的 nonce、timestamp、sign 是否均为合法的，如果不合法，则抛出对应的异常
     * @param paramMap 待校验的请求参数集合
     */
    public void checkParamMap(Map<String, Object> paramMap, String appSecret) {
        // 获取必须的三个参数
        Object timestampValue = paramMap.get(timestamp);
        Object nonceValue = paramMap.get(nonce);
        Object signValue = paramMap.get(sign);
        Object requestDataValue = paramMap.get(requestData);
        Object appIdValue = paramMap.get(appId);
        if (ObjectUtil.isEmpty(appSecret)){
            throw new NewxOpenException("appSecret 不能为空");
        }
        // 参数非空校验
        if(CollectionUtil.isEmpty(paramMap)){
            throw new NewxOpenException("请求参数为空");
        }
        if(ObjectUtil.isEmpty(appIdValue)){
            throw new NewxOpenException("缺少 appId 字段");
        }
        if(ObjectUtil.isEmpty(requestDataValue)){
            throw new NewxOpenException("缺少 requestData 字段");
        }
        if(ObjectUtil.isEmpty(timestampValue)){
            throw new NewxOpenException("缺少 timestamp 字段");
        }
        if(ObjectUtil.isEmpty(nonceValue)){
            throw new NewxOpenException("缺少 nonce 字段");
        }
        if(ObjectUtil.isEmpty(signValue)){
            throw new NewxOpenException("缺少 sign 字段");
        }
        // 依次校验三个参数
        checkTimestamp(Long.parseLong(timestampValue.toString()));
        checkNonce(nonceValue.toString(),appIdValue.toString());
        checkSign(paramMap, signValue.toString(),appSecret);

        // 通过 √
    }
    /**
     * 校验：指定时间戳与当前时间戳的差距是否在允许的范围内，如果超出则抛出异常
     * @param timestamp 待校验的时间戳
     */
    public void checkTimestamp(long timestamp) {
        if( ! isValidTimestamp(timestamp) ) {
            throw new NewxOpenException("timestamp 超出允许的范围：" + timestamp);
        }
    }
    /**
     * 判断：指定时间戳与当前时间戳的差距是否在允许的范围内
     * @param timestamp 待校验的时间戳
     * @return 是否在允许的范围内
     */
    public boolean isValidTimestamp(long timestamp) {
        long disparity = Math.abs(System.currentTimeMillis() - timestamp);
        return timestampDisparity == -1 || disparity <= timestampDisparity;
    }

    /**
     * 校验：随机字符串 nonce 是否有效，如果无效则抛出异常。
     * 		注意：同一 nonce 只可以被校验通过一次，校验后将保存在缓存中，再次校验将无法通过
     * @param nonce 待校验的随机字符串
     */
    public void checkNonce(String nonce,String appId) {
        // 为空代表无效
        if(ObjectUtil.isEmpty(nonce)) {
            throw new NewxOpenException("nonce 为空，无效");
        }
        StringRedisTemplate stringRedisTemplate = SpringContext.getBean(StringRedisTemplate.class);
        // 校验此 nonce 是否已被使用过
        if(ObjectUtil.isNotEmpty(stringRedisTemplate.opsForValue().get(splicingNonceSaveKey(nonce, appId)))){
            throw new NewxOpenException("此 nonce 已被使用过，不可重复使用：" + nonce);
        }
        // 校验通过后，将此 nonce 保存在缓存中，保证下次校验无法通过，此处需要加一个动态的key前缀，却别不同用户的请求，使用appId吧
        String key = splicingNonceSaveKey(nonce, appId);
        stringRedisTemplate.opsForValue().set(key, nonce, timestampDisparity * 2 + 2);
    }

    /**
     * 判断：给定的参数 + 秘钥 生成的签名是否为有效签名
     * @param paramsMap 参数列表
     * @param sign 待验证的签名
     * @param appSecret 秘钥
     * @return 签名是否有效
     */
    public boolean isValidSign(Map<String, Object> paramsMap, String sign,String appSecret) {
        String theSign = createSign(paramsMap,appSecret);
        return theSign.equals(sign);
    }

    /**
     * 校验：给定的参数 + 秘钥 生成的签名是否为有效签名，如果签名无效则抛出异常
     * @param paramsMap 参数列表
     * @param sign 待验证的签名
     * @param appSecret 秘钥
     */
    public void checkSign(Map<String, Object> paramsMap, String sign,String appSecret) {
        if( ! isValidSign(paramsMap, sign,appSecret) )  {
            throw new NewxOpenException("无效签名：" + sign);
        }
    }

    // ----------- 创建签名

    /**
     * 创建签名：md5(paramsStr + keyStr)
     * @param paramsMap 参数列表
     * @param appSecret 秘钥
     * @return 签名
     */
    public String createSign(Map<String, Object> paramsMap,String appSecret) {
        if (ObjectUtil.isEmpty(appSecret)){
            throw new NewxOpenException("参与参数签名的秘钥不可为空");
        }
        // 如果调用者不小心传入了 sign 参数，则此处需要将 sign 参数排除在外
        if(paramsMap.containsKey(sign)) {
            // 为了保证不影响原有的 paramsMap，此处需要再复制一份
            paramsMap = new TreeMap<>(paramsMap);
            paramsMap.remove(sign);
        }
        // 计算签名
        String paramsStr = joinParamsDictSort(paramsMap);
        String fullStr = paramsStr + "&" + key + "=" + appSecret;
        String signStr = abstractStr(fullStr);

        // 输入日志，方便调试
        log.debug("fullStr：{}", fullStr);
        log.debug("signStr：{}", signStr);

        // 返回
        return signStr;
    }

    /**
     * 使用摘要算法创建签名
     * @param fullStr 待摘要的字符串
     * @return 签名
     */
    public String abstractStr(String fullStr) {
        return SecureUtil.md5(fullStr);
    }

    /**
     * 将所有参数按照字典顺序连接成一个字符串，形如：a=18b=28c=3
     * @param paramsMap 参数列表
     * @return 拼接出的参数字符串
     */
    public String joinParamsDictSort(Map<String, Object> paramsMap) {
        // 保证字段按照字典顺序排列
        if( ! (paramsMap instanceof TreeMap) ) {
            paramsMap = new TreeMap<>(paramsMap);
        }

        // 拼接
        return joinParams(paramsMap);
    }

    // ----------- 拼接参数

    /**
     * 将所有参数连接成一个字符串(不排序)，形如：b=28a=18c=3
     * @param paramsMap 参数列表
     * @return 拼接出的参数字符串
     */
    public String joinParams(Map<String, ?> paramsMap) {

        // 按照 k1=v1&k2=v2&k3=v3 排列
        StringBuilder sb = new StringBuilder();
        for (String key : paramsMap.keySet()) {
            Object value = paramsMap.get(key);
            if( ! ObjectUtil.isEmpty(value) ) {
                sb.append(key).append("=").append(value).append("&");
            }
        }

        // 删除最后一位 &
        if(sb.length() > 0) {
            sb.deleteCharAt(sb.length() - 1);
        }

        // .
        return sb.toString();
    }

    // ------------------- 返回相应key -------------------

    /**
     * 拼接key：存储 nonce 时使用的 key
     * @param nonce nonce 值
     * @return key
     */
    public String splicingNonceSaveKey(String nonce,String appId) {
        return appId + ":sign:nonce:" + nonce;
    }
}
